package trabalho.sd.servidor;

/*

 Programa que roda na máquina servidor

 */
import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.jxta.discovery.DiscoveryEvent;
import net.jxta.discovery.DiscoveryListener;
import net.jxta.discovery.DiscoveryService;
import net.jxta.document.Advertisement;
import net.jxta.document.AdvertisementFactory;
import net.jxta.endpoint.Message;
import net.jxta.endpoint.StringMessageElement;
import net.jxta.exception.PeerGroupException;
import net.jxta.id.IDFactory;
import net.jxta.peer.PeerID;
import net.jxta.peergroup.PeerGroup;
import net.jxta.peergroup.PeerGroupID;
import net.jxta.pipe.PipeID;
import net.jxta.pipe.PipeMsgEvent;
import net.jxta.pipe.PipeMsgListener;
import net.jxta.pipe.PipeService;
import net.jxta.platform.NetworkConfigurator;
import net.jxta.platform.NetworkManager;
import net.jxta.protocol.DiscoveryResponseMsg;
import net.jxta.protocol.PipeAdvertisement;
import net.jxta.util.JxtaBiDiPipe;
import net.jxta.util.JxtaServerPipe;

public class ServidorCentral {

    public String nomeServidor;
    public int TcpPort = 9722;
    public boolean achouAdvPipePortador;
    public PeerID PID;
    public File ConfigurationFile;
    public PeerGroup NetPeerGroup;
    public PipeAdvertisement advPipeServidor;
    public ArrayList<RegistroArquivo> registroArquivos = new ArrayList<RegistroArquivo>();
    public DiscoveryService discService;

    public ServidorCentral() {

        try {

            Logger.getLogger("net.jxta").setLevel(Level.SEVERE);

            // Define o nome do servidor e seu PID
            nomeServidor = InetAddress.getLocalHost().getHostName();
            PID = IDFactory.newPeerID(PeerGroupID.worldPeerGroupID, nomeServidor.getBytes());
            ConfigurationFile = new File("." + System.getProperty("file.separator") + nomeServidor);
            NetworkManager.RecursiveDelete(ConfigurationFile);

            Ferramentas.CheckForExistingConfigurationDeletion(nomeServidor, ConfigurationFile);

            // Inicializa, configura e estabelece a conexão do peer na com o servidor
            NetworkManager MyNetworkManager = new NetworkManager(NetworkManager.ConfigMode.RENDEZVOUS,
                    nomeServidor, ConfigurationFile.toURI());

            NetworkConfigurator MyNetworkConfigurator = MyNetworkManager.getConfigurator();

            MyNetworkConfigurator.setTcpPort(TcpPort);
            MyNetworkConfigurator.setTcpEnabled(true);
            MyNetworkConfigurator.setTcpIncoming(true);
            MyNetworkConfigurator.setTcpOutgoing(true);
            MyNetworkConfigurator.setUseMulticast(false);
            MyNetworkConfigurator.setPeerID(PID);

            NetPeerGroup = MyNetworkManager.startNetwork();

            Ferramentas.GoToSleep(30);

            advPipeServidor = getPipeAdvertisementServidor();
            discService = NetPeerGroup.getDiscoveryService();

        } catch (PeerGroupException ex) {
            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
        } catch (IOException ex) {
            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    public PipeAdvertisement getPipeAdvertisementServidor() {


        PipeAdvertisement advPipeServidor = (PipeAdvertisement) AdvertisementFactory.newAdvertisement(PipeAdvertisement.getAdvertisementType());
        PipeID MyPipeID = IDFactory.newPipeID(PeerGroupID.defaultNetPeerGroupID, nomeServidor.getBytes());

        // Cria um novo pipe baseado nas informações do servidor
        advPipeServidor.setPipeID(MyPipeID);
        advPipeServidor.setType(PipeService.UnicastType);
        advPipeServidor.setName("PipeServidor");

        return advPipeServidor;
    }

    public void iniciaServidor() {

        try {

            discService.publish(advPipeServidor, DiscoveryService.INFINITE_LIFETIME, DiscoveryService.NO_EXPIRATION);
            discService.remotePublish(advPipeServidor);

            JxtaServerPipe pipeServidor = new JxtaServerPipe(NetPeerGroup, advPipeServidor);
            pipeServidor.setPipeTimeout(0);

            while (true) {

                System.out.println("\n********* Servidor " + nomeServidor + " **********");

                System.out.println("\nEsperando por requisicoes...");

                JxtaBiDiPipe biDiPipeConexao = pipeServidor.accept();

                // Inicia a thread que receberá as requisições ao servidor
                if (biDiPipeConexao != null) {

                    AtendimentoRequisicaoServidor atendReq = new AtendimentoRequisicaoServidor(biDiPipeConexao);

                    atendReq.start();
                }
            }

        } catch (IOException ex) {
            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
        }

    }

    public class AtendimentoRequisicaoServidor extends Thread {

        JxtaBiDiPipe biDiPipeConexao;

        public AtendimentoRequisicaoServidor(JxtaBiDiPipe biDiPipeConexao) {

            this.biDiPipeConexao = biDiPipeConexao;
        }

        @Override
        // Thread que aguarda a mensagem de requisição
        public void run() {

            biDiPipeConexao.setMessageListener(new RecebedorMensagemPipeServidor(biDiPipeConexao));

        }
    }

    public class RecebedorMensagemPipeServidor implements PipeMsgListener {

        public PipeAdvertisement advPipePeerPortador;
        public JxtaBiDiPipe pipePeerRequisitante;
        public boolean portadorDisponivelEncontrado;

        public RecebedorMensagemPipeServidor(JxtaBiDiPipe pipePeerRequisitante) {

            this.pipePeerRequisitante = pipePeerRequisitante;
        }

        public String listaArquivosCompartilhados(boolean listarNaTela) {

            String listaArqs = "";

            if (!registroArquivos.isEmpty()) {

                listaArqs += "Arquivos existentes na rede: ";

                for (RegistroArquivo regArq : registroArquivos) {

                    listaArqs += "\n" + regArq.nomeArquivo + " : " + regArq.tamanhoArquivo + " bytes" + ". Portadores: ";

                    for (String nomePort : regArq.peersPortadores) {

                        listaArqs += "  " + nomePort;
                    }
                }

            } else {

                listaArqs += "Nao ha arquivos compartilhados na rede no momento.";
            }

            if (listarNaTela) {

                System.out.println(listaArqs);
            }

            return listaArqs;
        }

        @Override
        public synchronized void pipeMsgEvent(PipeMsgEvent pipeMsgEvent) {

            Message mensagemRecebida = pipeMsgEvent.getMessage();

            Message mensagemResposta = new Message();
            StringMessageElement respostaAoRequisitante;

            String comandoServidor = mensagemRecebida.getMessageElement("ComandoServidor").toString();

            StringMessageElement comandoPortador;

            RegistroArquivo regArq = new RegistroArquivo();

            // Compõe um novo registro de arquivo com o nome do arquivo e o peer que mandou a solicitação
            regArq.nomeArquivo = mensagemRecebida.getMessageElement("NomeArquivo").toString();
            regArq.peersPortadores.add(mensagemRecebida.getMessageElement("PeerRemetente").toString());

            switch (comandoServidor.charAt(0)) {

                // Caso o comando ao servidor seja de adicionar um novo arquivo ao registro de arquivos disponíveis
                case 'N':

                    System.out.println("Novo arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString()
                            + " informado por " + mensagemRecebida.getMessageElement("PeerRemetente").toString() + ".");

                    // Incrementa o novo registro de arquivo com o tamanho e data de modificação do arquivo
                    regArq.tamanhoArquivo = Long.parseLong(mensagemRecebida.getMessageElement("TamanhoArquivo").toString());
                    regArq.dataModifArquivo = Long.parseLong(mensagemRecebida.getMessageElement("DataModifArquivo").toString());

                    // Caso o arquivo que deseja-se adicionar não conste no registro de arquivos, adiciona o registro temporário a lista do servidor
                    if (!registroArquivos.contains(regArq)) {
                        registroArquivos.add(regArq);

                        // Caso o arquivo já exista na lista do servidor, adiciona o peer que enviou a solicitação à lista de possuidores do arquivo
                    } else {

                        for (RegistroArquivo r : registroArquivos) {

                            if (r.equals(regArq)) {

                                r.peersPortadores.add(mensagemRecebida.getMessageElement("PeerRemetente").toString());
                            }
                        }
                    }

                    // Configura a mensagem que será enviada ao peer solicitador notificando a inclusão do novo arquivo
                    comandoPortador = new StringMessageElement("ComandoPortador", "RespostaAoRequisitante", null);
                    mensagemResposta.addMessageElement(comandoPortador);

                    respostaAoRequisitante = new StringMessageElement(
                            "RespostaAoRequisitante", "Novo arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString() + " registrado com sucesso.", null);
                    mensagemResposta.addMessageElement(respostaAoRequisitante);

                    try {
                        pipePeerRequisitante.sendMessage(mensagemResposta);
                    } catch (IOException ex) {
                        Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                    }

                    listaArquivosCompartilhados(true);

                    break;


                // Caso o comando enviado ao servidor seja de que um peer atualizou um arquivo
                case 'A':

                    // Obtém as informaçoes de tamanho e data de modificação do arquivo atualizado
                    regArq.tamanhoArquivo = Long.parseLong(mensagemRecebida.getMessageElement("TamanhoArquivo").toString());
                    regArq.dataModifArquivo = Long.parseLong(mensagemRecebida.getMessageElement("DataModifArquivo").toString());

                    System.out.println("Atualizacao do arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString()
                            + " informada por " + mensagemRecebida.getMessageElement("PeerRemetente").toString() + ".");

                    ArrayList<String> peersPortadoresArqAntigo = new ArrayList<String>();

                    // Obtém uma lista dos arquivos que possuem a versao antiga do dado arquivo
                    for (RegistroArquivo r : registroArquivos) {

                        if (r.equals(regArq)) {

                            for (String p : r.peersPortadores) {
                                peersPortadoresArqAntigo.add(p);
                            }

                            r.tamanhoArquivo = regArq.tamanhoArquivo;
                            r.dataModifArquivo = regArq.dataModifArquivo;
                            r.peersPortadores.clear();
                            r.peersPortadores.add(mensagemRecebida.getMessageElement("PeerRemetente").toString());
                        }
                    }

                    peersPortadoresArqAntigo.remove(mensagemRecebida.getMessageElement("PeerRemetente").toString());

                    // Para cada peer que tem o arquivo antigo..
                    for (String peerPortador : peersPortadoresArqAntigo) {

                        // Obtém o advertisement do dado peer
                        try {

                            Enumeration<Advertisement> enumAdvPortadores = discService.getLocalAdvertisements(DiscoveryService.ADV, "Name", "Pipe" + peerPortador);
                            advPipePeerPortador = (PipeAdvertisement) enumAdvPortadores.nextElement();

                        } catch (IOException ex) {
                            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                        }

                        try {
                            JxtaBiDiPipe pipePortador = new JxtaBiDiPipe(NetPeerGroup, advPipePeerPortador, 60, new MsgListenerRespostaPortador(peerPortador, pipePeerRequisitante));

                            Message mensagemPortador = new Message();

                            // Configura a mensagem que será enviada ao peer notificando a atualização de um arquivo
                            comandoPortador = new StringMessageElement(
                                    "ComandoPortador", "AtualizacaoArquivo", null);

                            StringMessageElement peerAtualizador = new StringMessageElement(
                                    "PeerAtualizador", mensagemRecebida.getMessageElement("PeerRemetente").toString(), null);

                            StringMessageElement nomeArquivo = new StringMessageElement(
                                    "NomeArquivo", regArq.nomeArquivo, null);

                            // Compõe a nova mensagem
                            mensagemPortador.addMessageElement(comandoPortador);
                            mensagemPortador.addMessageElement(peerAtualizador);
                            mensagemPortador.addMessageElement(nomeArquivo);

                            // Envia a mensagem ao peer, notificando a alteração de um arquivo
                            pipePortador.sendMessage(mensagemPortador);

                        } catch (IOException ex) {
                            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                        }

                    }
                    // Configura a mensagem que será enviada ao peer solicitante notificando a confirmação da atualização
                    comandoPortador = new StringMessageElement("ComandoPortador", "RespostaAoRequisitante", null);

                    mensagemResposta.addMessageElement(comandoPortador);
                    respostaAoRequisitante = new StringMessageElement(
                            "RespostaAoRequisitante", "Atualizacao do arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString()
                            + " registrada com sucesso. Os demais portadores do arquivo serao solicitados a obter sua versao mais atual.", null);

                    mensagemResposta.addMessageElement(respostaAoRequisitante);

                    try {
                        pipePeerRequisitante.sendMessage(mensagemResposta);
                    } catch (IOException ex) {
                        Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                    }

                    listaArquivosCompartilhados(true);

                    break;

                // Caso a mensagem seja de que um peer excluiu um arquivo
                case 'R':

                    RegistroArquivo regArqRemovido = null;

                    System.out.println("Remocao do arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString()
                            + " informada por " + mensagemRecebida.getMessageElement("PeerRemetente").toString() + ".");

                    for (RegistroArquivo r : registroArquivos) {

                        // Exclui o peer em questão da lista de possuidores do arquivo removido
                        if (r.equals(regArq)) {

                            regArqRemovido = r;
                        }
                    }
                    if (regArqRemovido
                            != null) {

                        if (regArqRemovido.peersPortadores.contains(mensagemRecebida.getMessageElement("PeerRemetente").toString())) {

                            regArqRemovido.peersPortadores.remove(mensagemRecebida.getMessageElement("PeerRemetente").toString());

                            // Caso esse seja o único peer com o arquivo em questão, remove o arquivo da lista de registros do servidor
                            if (regArqRemovido.peersPortadores.isEmpty()) {

                                registroArquivos.remove(regArqRemovido);
                            }

                            // Configura a mensagem que será enviada ao peer solicitante notificando a confirmação da exclusão
                            comandoPortador = new StringMessageElement("ComandoPortador", "RespostaAoRequisitante", null);
                            mensagemResposta.addMessageElement(comandoPortador);
                            respostaAoRequisitante = new StringMessageElement(
                                    "RespostaAoRequisitante", "Remocao do arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString() + " registrada com sucesso.", null);
                            mensagemResposta.addMessageElement(respostaAoRequisitante);

                            try {
                                pipePeerRequisitante.sendMessage(mensagemResposta);
                            } catch (IOException ex) {
                                Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                            }

                        }
                    }

                    listaArquivosCompartilhados(true);

                    break;

                // Caso a mensagem seja de solicitação de download de um arquivo
                case 'D':

                    System.out.println("Download do arquivo " + mensagemRecebida.getMessageElement("NomeArquivo").toString()
                            + " solicitado por " + mensagemRecebida.getMessageElement("PeerRemetente").toString() + ".");

                    RegistroArquivo rArqDownload = null;
                    // Verifica se o arquivo requisitado existe no registro do servidor
                    for (RegistroArquivo r : registroArquivos) {

                        if (r.equals(regArq)) {

                            rArqDownload = r;
                        }
                    }

                    // Caso o arquivo exista
                    if (rArqDownload != null) {

                        portadorDisponivelEncontrado = false;

                        try {

                            for (String nomePeerPortador : rArqDownload.peersPortadores) {

                                if (!portadorDisponivelEncontrado) {

                                    int numTentativaConPortador = 1;
                                    achouAdvPipePortador = false;

                                    try {

                                        Enumeration<Advertisement> enumAdvPortadores = discService.getLocalAdvertisements(DiscoveryService.ADV, "Name", "Pipe" + nomePeerPortador);
                                        advPipePeerPortador = (PipeAdvertisement) enumAdvPortadores.nextElement();

                                    } catch (IOException ex) {
                                        System.out.println("Erro ao encontrar advertisement de pipe.");
                                    }

                                    try {

                                        System.out.println("Requisitando download a " + nomePeerPortador + ". Tentativa " + numTentativaConPortador++ + "...");
                                        testaDownloadPortador(nomePeerPortador, rArqDownload);

                                        Ferramentas.GoToSleep(5);

                                    } catch (IOException ex) {

                                        try {

                                            System.out.println("Requisitando download a " + nomePeerPortador + ". Tentativa " + numTentativaConPortador++ + "...");
                                            testaDownloadPortador(nomePeerPortador, rArqDownload);

                                            Ferramentas.GoToSleep(5);

                                        } catch (IOException ex1) {

                                            try {

                                                System.out.println("Requisitando download a " + nomePeerPortador + ". Tentativa " + numTentativaConPortador++ + "...");
                                                testaDownloadPortador(nomePeerPortador, rArqDownload);

                                                Ferramentas.GoToSleep(5);

                                            } catch (IOException ex2) {

                                                System.out.println("Erro ao se conectar a " + nomePeerPortador);
                                                Ferramentas.GoToSleep(5);
                                            }
                                        }
                                    }
                                }
                            }

                        } catch (java.util.ConcurrentModificationException concEx) {

                            System.out.print("");

                        }

                        if (!portadorDisponivelEncontrado) {

                            Message respostaRequisitante = new Message();

                            // Configura a mensagem que será enviada ao peer solicitante, com as informações vindas do peer possuidor do arquivo
                            StringMessageElement peerRemetente = new StringMessageElement(
                                    "PeerRemetente", nomeServidor, null);

                            // Configura a mensagem que será enviada ao peer solicitante, dizendo que o arquivo não está disponível
                            StringMessageElement comandoRequisitante = new StringMessageElement(
                                    "RespostaAoRequisitante", "ErroAoObterArquivo", null);

                            // Compõe a nova mensagem
                            respostaRequisitante.addMessageElement(peerRemetente);

                            // Compõe a nova mensagem
                            respostaRequisitante.addMessageElement(comandoRequisitante);

                            try {
                                pipePeerRequisitante.sendMessage(respostaRequisitante);
                            } catch (IOException ex) {
                                Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                            }
                        }

                    } else {

                        Message respostaRequisitante = new Message();

                        // Configura a mensagem que será enviada ao peer solicitante, com as informações vindas do peer possuidor do arquivo
                        StringMessageElement peerRemetente = new StringMessageElement(
                                "PeerRemetente", nomeServidor, null);

                        // Compõe a nova mensagem
                        respostaRequisitante.addMessageElement(peerRemetente);

                        StringMessageElement comandoRequisitante = new StringMessageElement(
                                "RespostaAoRequisitante", "ArquivoNaoEncotrado", null);

                        // Compõe a nova mensagem
                        respostaRequisitante.addMessageElement(comandoRequisitante);

                        try {
                            pipePeerRequisitante.sendMessage(respostaRequisitante);
                        } catch (IOException ex) {
                            Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                        }
                    }

                    break;

                case 'L':

                    // Configura a mensagem que será enviada ao peer solicitante com a lista de arquivos compartilhados na rede
                    comandoPortador = new StringMessageElement("ComandoPortador", "RespostaAoRequisitante", null);

                    mensagemResposta.addMessageElement(comandoPortador);
                    respostaAoRequisitante = new StringMessageElement(
                            "RespostaAoRequisitante", listaArquivosCompartilhados(false), null);

                    mensagemResposta.addMessageElement(respostaAoRequisitante);


                    try {
                        pipePeerRequisitante.sendMessage(mensagemResposta);
                    } catch (IOException ex) {
                        Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                    }

                    break;
            }
        }

        public void testaDownloadPortador(String nomePeerPortador, RegistroArquivo rArqDownload) throws IOException {

            JxtaBiDiPipe pipePortador = new JxtaBiDiPipe(NetPeerGroup, advPipePeerPortador, 60, new MsgListenerRespostaPortador(nomePeerPortador, pipePeerRequisitante));

            Message mensagemPortador = new Message();

            // Configura a mensagem que será enviada ao peer possuidor do arquivo requisitando-o
            StringMessageElement comandoPortador = new StringMessageElement(
                    "ComandoPortador", "TestePortador", null);

            StringMessageElement peerRemetente = new StringMessageElement(
                    "PeerRemetente", nomeServidor, null);

            StringMessageElement nomeArquivo = new StringMessageElement(
                    "NomeArquivo", rArqDownload.nomeArquivo, null);

            // Compõe a nova mensagem
            mensagemPortador.addMessageElement(comandoPortador);
            mensagemPortador.addMessageElement(peerRemetente);
            mensagemPortador.addMessageElement(nomeArquivo);

            // Envia a mensagem ao peer possuidor
            pipePortador.sendMessage(mensagemPortador);

        }

        public class MsgListenerRespostaPortador implements PipeMsgListener {

            public String nomePortador;
            public JxtaBiDiPipe pipePeerRequisitante;

            public MsgListenerRespostaPortador(String nomePortador, JxtaBiDiPipe pipePeerRequisitante) {

                this.nomePortador = nomePortador;
                this.pipePeerRequisitante = pipePeerRequisitante;
            }

            @Override
            public void pipeMsgEvent(PipeMsgEvent pipeMsgEvent) {

                Message mensagemRecebida = pipeMsgEvent.getMessage();

                String respostaPortador = mensagemRecebida.getMessageElement("RespostaPortador").toString();

                Message respostaAoRequisitante = new Message();

                // Configura a mensagem que será enviada ao peer solicitante, com as informações vindas do peer possuidor do arquivo
                StringMessageElement peerRemetente = new StringMessageElement(
                        "PeerRemetente", nomeServidor, null);

                // Compõe a nova mensagem
                respostaAoRequisitante.addMessageElement(peerRemetente);

                StringMessageElement comandoRequisitante;

                // Caso o peer possuidor envie uma mensagem de que o arquivo solicitado está disponível
                if (respostaPortador.equals("ArquivoDisponivel")) {

                    RecebedorMensagemPipeServidor.this.portadorDisponivelEncontrado = true;

                    // Configura a mensagem que será enviada ao peer solicitante, com a resposta do peer possuidor do arquivo e seu nome
                    comandoRequisitante = new StringMessageElement(
                            "RespostaAoRequisitante", "ArquivoDisponivel", null);

                    StringMessageElement peerPortador = new StringMessageElement(
                            "PeerPortador", nomePortador, null);

                    // Compõe a nova mensagem
                    respostaAoRequisitante.addMessageElement(peerPortador);

                    // Compõe a nova mensagem
                    respostaAoRequisitante.addMessageElement(comandoRequisitante);

                    try {
                        pipePeerRequisitante.sendMessage(respostaAoRequisitante);
                    } catch (IOException ex) {
                        Logger.getLogger(ServidorCentral.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        }
    }

    public class RegistroArquivo {

        public String nomeArquivo;
        public long tamanhoArquivo;
        public long dataModifArquivo;
        public ArrayList<String> peersPortadores = new ArrayList<String>();

        @Override
        // Implementa a função comparativa de igualdade: Define que dois arquivos são iguais se possuem o mesmo nome
        public boolean equals(Object o) {

            if (o instanceof RegistroArquivo) {

                RegistroArquivo regExterno = (RegistroArquivo) o;

                if (regExterno.nomeArquivo.equals(this.nomeArquivo)) {
                    return true;
                }
            }

            return false;
        }

        @Override
        public int hashCode() {
            int hash = 5;
            hash = 37 * hash + this.nomeArquivo.hashCode();
            return hash;
        }
    }
}